(function($) { // Create closure.
	// Constructor for swiftuipage object.
	
	function SwiftUIPage(options) {
		// Public properties of swiftuipage.
		this.element = this.empty();
		this.ready = false;
		this.columns = Array();
		this.portlets = Array();
		// End of public properties of swiftuipage.
		/**
		 * Private properties of swiftuipage.
		 */

		// Used to determine whether there are any incomplete ajax requests
		// pending initialization of the swiftuipage.
		this.asynchronousRequestCounter = 0;

		// Used to determine whether two resort events are resulting from the
		// same UI event.
		this.currentReSortEvent = null;

		this.initPage(options);
	};
	
	var fn = SwiftUIPage.prototype;
	
	
  // Public static properties of dashboard. Default settings.
  fn.dashboard.defaults = {
    columns: 3,
    emptyPlaceholderInner: 'There are no widgets in this column of your dashboard.',
    fullscreenHeaderInner: '<img alt="Close this widget" src="images/close.gif" /> Return to Dashboard',
    throbberMarkup: '<div class="throbber"><img alt="Loading, please wait" src="images/throbber.gif" /><p>Loading...</p></div>',
    animationSpeed: 200,
    callbacks: {},
    widgetCallbacks: {}
  };

  // Default widget settings.
  fn.dashboard.widget = {
    defaults: {
      minimized: false,
      settings: false,
      fullscreen: false
    }
  };	
	
  
	// @todo use an event library to register, bind to and invoke events.
	// @param callback is a function.
	// @param theThis is the context given to that function when it
	// executes. It becomes 'this' inside of that function.
	fn.invokeCallback = function(callback, theThis, parameterOne) {
		if (callback) {
			callback.call(theThis, parameterOne);
		}
	};

	/**
	 * Called on init
	 */
	fn.initPage = function(options){
		// Merge in the caller's options with the defaults.
		this.opts = $.extend({}, fn.dashboard.defaults, options);

		// Execution 'forks' here and restarts in init(). Tell the user we're
		// busy with a throbber.
		this.throbber = $(opts.throbberMarkup).appendTo(this.element);

		$.getJSON(this.opts.ajaxCallbacks.getWidgetsByColumn.url,
				this.opts.ajaxCallbacks.getWidgetsByColumn.data, this.onPageDefLoad);
		this.asynchronousRequestCounter++;
		return this;
		// End of constructor and private properties for swiftuipage object.

	};
	
	
	// Ajax callback for getWidgetsByColumn.
	fn.onPageDefLoad = function(portlets, status) {
		this.asynchronousRequestCounter--;
		this.throbber.remove();
		var markup = '<li class="empty-placeholder">'
				+ opts.emptyPlaceholderInner + '</li>';

		// Build the swiftuipage in the DOM. For each column...
		// (Don't iterate on portlets since this will break badly if the
		// dataset has empty columns.)
		for ( var c = 0; c < opts.columns; c++) {
			// Save the column to both the public scope for external
			// accessibility and the local scope for readability.
			var col = swiftuipage.columns[c] = {
				initialWidgets : Array(),
				element : $(
						'<ul id="column-' + c + '" class="column"></ul>')
						.appendTo(swiftuipage.element)
			};

			// Add the empty placeholder now, hide it and save it.
			col.emptyPlaceholder = $(markup).appendTo(col.element).hide();

			// For each portlet in this column.
			for ( var id in portlets[c]) {
				// Build a new portlet object and save it to various publicly
				// accessible places.
				col.initialWidgets[id] = this.portlets[id] = portlet({
					id : id,
					element : $('<li class="portlet"></li>').appendTo(
							col.element),
					initialColumn : col,
					minimized : portlets[c][id]
				});
			}
		}

		this.invokeCallback(this.opts.callbacks.init, this);
	};

	// Contructors for each portlet call this when initialization has
	// finished so that swiftuipage can complete it's intitialization.
	fn.completeInit= function() {
		// Don't do anything if any portlets are waiting for ajax requests to
		// complete in order to finish initialization.
		if (this.asynchronousRequestCounter > 0) {
			return;
		}

		// Make portlets sortable across columns.
		this..sortableElement = $('.column').sortable({
			connectWith : [ '.column' ],

			// The class of the element by which portlets are draggable.
			handle : '.portlet-header',

			// The class of placeholder elements (the 'ghost' portlet showing
			// where the dragged item would land if released now.)
			placeholder : 'placeholder',

			// This worked in jQuery UI 1.6 rc5, but broke in rc6.
			// @todo: Test this and/or report sortable's opacity bug.
			// opacity: 0.2,

			// Maks sure that only portlets are sortable, and not empty
			// placeholders.
			items : '> .portlet',
			forcePlaceholderSize : true,

			// Callback functions.
			update : resorted,
			start : hideEmptyPlaceholders
		});

		// Update empty placeholders.
		this.saveColumns();
		this.ready = true;
		this.invokeCallback(opts.callbacks.ready, this);
	};
		
	
	
	// Saves the order of portlets for all columns including the
	// portlet.minimized status to options.ajaxCallbacks.saveColumns.
	fn.saveColumns = function() {
		// Update the display status of the empty placeholders.
		for ( var c in this.columns) {
			var col = this.columns[c];
			// Are there any visible children of the column (excluding the
			// empty placeholder)?
			if (col.element.children(':visible').not(col.emptyPlaceholder).length > 0) {
				col.emptyPlaceholder.hide();
			} else {
				col.emptyPlaceholder.show();
			}
		}

		// Don't save any changes to the server unless the swiftuipage has
		// finished initiating.
		if (!this.ready) {
			return;
		}

		// Build a list of params to post to the server.
		var params = {};

		// For each column...
		for ( var c in this.columns) {

			// IDs of the sortable elements in this column.
			var ids = this.columns[c].element.sortable('toArray');

			// For each id...
			for ( var w in ids) {
				// Chop 'portlet-' off of the front so that we have the real
				// portlet id.
				var id = ids[w].substring('portlet-'.length);

				// Add one flat property to the params object that will look
				// like an array element to the PHP server.
				// Unfortunately jQuery doesn't do this for us.
				params['columns[' + c + '][' + id + ']'] = (this.portlets[id].minimized ? '1'
						: '0');
			}
		}

		// The ajaxCallback settings overwrite any duplicate properties.
		$.extend(params, opts.ajaxCallbacks.saveColumns.data);
		$.post(opts.ajaxCallbacks.saveColumns.url, params, function(
				response, status) {
			invokeCallback(opts.callbacks.saveColumns, this);

			// Log the response to aid server-side debugging.
			if (window.console && console.log) {
				console.log(response);
			}
		});
	};
	
	
	// Puts the swiftuipage into full screen mode, saving element for when the
	// user exits full-screen mode.
	// Does not add element to the DOM – this is the caller's
	// responsibility.
	// Does show and hide element though.
	fn.enterFullscreen = function(element) {
		// Hide the columns.
		for ( var c in this.columns) {
			this.columns[c].element.hide();
		}

		if (!this.fullscreen) {
			// Initialize.
			var markup = '<a id="full-screen-header" class="full-screen-close-icon">'
					+ opts.fullscreenHeaderInner + '</a>';
			this.fullscreen = {
				headerElement : $(markup).prependTo(this.element)
						.click(this.exitFullscreen).hide()
			};
		}

		this.fullscreen.headerElement.slideDown();
		this.fullscreen.currentElement = element.show();
		this.fullscreen.displayed = true;
		invokeCallback(opts.callbacks.enterFullscreen, this,
				this.fullscreen.currentElement);
	};

	// Takes the swiftuipage out of full screen mode, hiding the active
	// fullscreen element.
	fn.exitFullscreen = function() {
		if (!this.fullscreen.displayed) {
			return;
		}

		this.fullscreen.headerElement.slideUp();
		this.fullscreen.currentElement.hide();
		this.fullscreen.displayed = false;

		// Show the columns.
		for ( var c in this.columns) {
			this.columns[c].element.show();
		}

		invokeCallback(opts.callbacks.exitFullscreen, this,
				this.fullscreen.currentElement);
	};
	// End of public methods of swiftuipage.
	

	
	// Callback for when any list has changed (and the user has finished
	// resorting).
	function resorted(e, ui) {
		// Only do anything if we haven't already handled resorts based on
		// changes from this UI DOM event.
		// (resorted() gets invoked once for each list when an item is moved
		// from one to another.)
		if (!currentReSortEvent || e.originalEvent != currentReSortEvent) {
			currentReSortEvent = e.originalEvent;
			swiftuipage.saveColumns();
		}
	};

	// Callback for when a user starts resorting a list. Hides all the empty
	// placeholders.
	function hideEmptyPlaceholders(e, ui) {
		for ( var c in swiftuipage.columns) {
			swiftuipage.columns[c].emptyPlaceholder.hide();
		}
	};	
	
	
	
	/**
	 * portlet object Private sub-class of swiftuipage Constructor starts
	 */
	function portlet(portlet) {
		// Merge default options with the options defined for this portlet.
		portlet = $.extend({}, fn.swiftuipage.portlet.defaults, portlet);

		/**
		 * Public methods of portlet.
		 */

		// Toggles the minimize() & maximize() methods.
		portlet.toggleMinimize = function() {
			if (portlet.minimized) {
				portlet.maximize();
			} else {
				portlet.minimize();
			}

			portlet.hideSettings();
			swiftuipage.saveColumns();
		};
		portlet.minimize = function() {
			$('.portlet-content', portlet.element).slideUp(
					opts.animationSpeed);
			var img = $('img', portlet.controls.minimize.element);
			img
					.attr('src', img.attr('src').replace('minimize',
							'maximize'));
			portlet.minimized = true;
		};
		portlet.maximize = function() {
			$('.portlet-content', portlet.element).slideDown(
					opts.animationSpeed);
			var img = $('img', portlet.controls.minimize.element);
			img
					.attr('src', img.attr('src').replace('maximize',
							'minimize'));
			portlet.minimized = false;
		};

		// Toggles whether the portlet is in settings-display mode or not.
		portlet.toggleSettings = function() {
			if (portlet.settings.displayed) {
				// Widgets always exit settings into maximized state.
				portlet.maximize();
				portlet.hideSettings();
				invokeCallback(opts.portletCallbacks.hideSettings, portlet);
			} else {
				portlet.minimize();
				portlet.showSettings();
				invokeCallback(opts.portletCallbacks.showSettings, portlet);
			}
		};
		portlet.showSettings = function() {
			if (portlet.settings.element) {
				portlet.settings.element.show();

				// Settings are loaded via AJAX. Only execute the script if
				// the settings have been loaded.
				if (portlet.settings.ready) {
					getJavascript(portlet.settings.script);
				}
			} else {
				// Settings have not been initialized. Do so now.
				initSettings();
			}
			portlet.settings.displayed = true;
		};
		portlet.hideSettings = function() {
			if (portlet.settings.element) {
				portlet.settings.element.hide();
			}
			portlet.settings.displayed = false;
		};
		portlet.saveSettings = function() {
			// Build list of parameters to POST to server.
			var params = {};
			// serializeArray() returns an array of objects. Process it.
			var fields = portlet.settings.element.serializeArray();
			for ( var i in fields) {
				var field = fields[i];
				// Put the values into flat object properties that PHP will
				// parse into an array server-side.
				// (Unfortunately jQuery doesn't do this)
				params['settings[' + field.name + ']'] = field.value;
			}

			// Things get messy here.
			// @todo Refactor to use currentState and targetedState
			// properties to determine what needs
			// to be done to get to any desired state on any UI or AJAX
			// event – since these don't always
			// match.
			// E.g. When a user starts a new UI event before the Ajax event
			// handler from a previous
			// UI event gets invoked.

			// Hide the settings first of all.
			portlet.toggleSettings();
			// Save the real settings element so that we can restore the
			// reference later.
			var settingsElement = portlet.settings.element;
			// Empty the settings form.
			portlet.settings.innerElement.empty();
			initThrobber();
			// So that showSettings() and hideSettings() can do SOMETHING,
			// without showing the empty settings form.
			portlet.settings.element = portlet.throbber.hide();
			portlet.settings.ready = false;

			// Save the settings to the server.
			$.extend(params, opts.ajaxCallbacks.portletSettings.data, {
				id : portlet.id
			});
			$.post(opts.ajaxCallbacks.portletSettings.url, params, function(
					response, status) {
				// Merge the response into portlet.settings.
				$.extend(portlet.settings, response);
				// Restore the reference to the real settings element.
				portlet.settings.element = settingsElement;
				// Make sure the settings form is empty and add the updated
				// settings form.
				portlet.settings.innerElement.empty().append(
						portlet.settings.markup);
				portlet.settings.ready = true;

				// Did the user already jump back into settings-display mode
				// before we could finish reloading the settings form?
				if (portlet.settings.displayed) {
					// Ooops! We had better take care of hiding the throbber
					// and showing the settings form then.
					portlet.throbber.hide();
					portlet.showSettings();
					invokeCallback(opts.portletCallbacks.saveSettings,
							swiftuipage);
				}
			}, 'json');

			// Don't let form submittal bubble up.
			return false;
		};

		portlet.enterFullscreen = function() {
			// Make sure the portlet actually supports full screen mode.
			if (!portlet.fullscreen) {
				return;
			}

			if (!portlet.fullscreen.element) {
				// Initialize the full screen element for this portlet.
				var markup = '<div id="portlet-' + portlet.id
						+ '-full-screen">' + portlet.fullscreen + '</div>';

				// Overwrite the portlet.fullscreen string.
				portlet.fullscreen = {
					initialMarkup : portlet.fullscreen,
					element : $(markup).appendTo(swiftuipage.element)
				};

				getJavascript(portlet.fullscreenInitScript);
			}

			// Let swiftuipage.enterFullscreen() do the heavy lifting.
			swiftuipage.enterFullscreen(portlet.fullscreen.element);
			getJavascript(portlet.fullscreenScript);
			portlet.fullscreen.displayed = true;
		};
		// Exit fullscreen mode.
		portlet.exitFullscreen = function() {
			// This is just a wrapper for swiftuipage.exitFullscreen() which
			// does the heavy lifting.
			swiftuipage.exitFullscreen();
		};

		// Adds controls to a portlet. id is for internal use and image file
		// name in images/ (a .gif).
		portlet.addControl = function(id, control) {
			var markup = '<a class="portlet-icon ' + id
					+ '-icon"><img src="images/' + id + '.gif" alt="'
					+ control.description + '" /></a>';
			control.element = $(markup).prependTo(
					$('.portlet-controls', portlet.element)).click(
					control.callback);
		};

		// An external method used only by and from external scripts to
		// reload content. Not invoked or used internally.
		// The portlet must provide the script that executes this, as well as
		// the script that invokes it.
		portlet.reloadContent = function() {
			getJavascript(portlet.reloadContentScript);
			invokeCallback(opts.portletCallbacks.reloadContent, portlet);
		};

		// Removes the portlet from the swiftuipage, and saves columns.
		portlet.remove = function() {
			invokeCallback(opts.portletCallbacks.remove, portlet);
			portlet.element.fadeOut(opts.animationSpeed, function() {
				$(this).remove();
				swiftuipage.saveColumns();
			});
		};
		// End public methods of portlet.

		/**
		 * Public properties of portlet.
		 */

		// Default controls. External script can add more with
		// portlet.addControls()
		portlet.controls = {
			settings : {
				description : 'Configure this portlet',
				callback : portlet.toggleSettings
			},
			minimize : {
				description : 'Show & hide this portlet',
				callback : portlet.toggleMinimize
			},
			fullscreen : {
				description : 'Open portlet to full screen mode',
				callback : portlet.enterFullscreen
			},
			close : {
				description : 'Remove this portlet',
				callback : portlet.remove
			}
		};
		// End public properties of portlet.

		/**
		 * Private properties of portlet.
		 */

		// We're gonna 'fork' execution again, so let's tell the user to
		// hold with us till the AJAX callback gets invoked.
		var throbber = $(opts.throbberMarkup).appendTo(portlet.element);
		var params = $.extend({}, opts.ajaxCallbacks.getWidget.data, {
			id : portlet.id
		});
		$.getJSON(opts.ajaxCallbacks.getWidget.url, params, init);

		// Help swiftuipage track whether we've got any outstanding requests
		// on which initialization is pending.
		asynchronousRequestCounter++;
		return portlet;
		// End of private properties of portlet.

		/**
		 * Private methods of portlet.
		 */

		// Ajax callback for portlet initialization.
		function init(data, status) {
			asynchronousRequestCounter--;
			$.extend(portlet, data);

			// Delete controls that don't apply to this portlet.
			if (!portlet.settings) {
				delete portlet.controls.settings;
			}
			if (!portlet.fullscreen) {
				delete portlet.controls.fullscreen;
			}

			portlet.element.attr('id', 'portlet-' + portlet.id).addClass(
					portlet.classes);
			throbber.remove();
			// Build and add the portlet's DOM element.
			$(portletHTML()).appendTo(portlet.element);
			// Save the content element so that external scripts can reload
			// it easily.
			portlet.contentElement = $('.portlet-content', portlet.element);
			$.each(portlet.controls, portlet.addControl);

			// Switch the initial state so that it initializes to the
			// correct state.
			portlet.minimized = !portlet.minimized;
			portlet.toggleMinimize();
			getJavascript(portlet.initScript);
			invokeCallback(opts.portletCallbacks.get, portlet);

			// completeInit() is a private method of the swiftuipage. Let it
			// complete initialization of the swiftuipage.
			completeInit();
		}

		// Builds inner HTML for portlets.
		function portletHTML() {
			var html = '';
			html += '<div class="portlet-wrapper">';
			html += '  <div class="portlet-controls"></div>';
			html += '  <div class="portlet-header">' + portlet.title
					+ '</div>';
			html += '  <div class="portlet-content">' + portlet.content
					+ '</div>';
			html += '</div>';
			return html;
		}

		// Initializes a portlets settings pane.
		function initSettings() {
			// Overwrite portlet.settings (boolean).
			initThrobber();
			portlet.settings = {
				element : portlet.throbber.show(),
				ready : false
			};

			// Get the settings markup and script executables for this
			// portlet.
			var params = $.extend({},
					opts.ajaxCallbacks.portletSettings.data, {
						id : portlet.id
					});
			$
					.getJSON(opts.ajaxCallbacks.portletSettings.url, params,
							function(response, status) {
								$.extend(portlet.settings, response);
								// Build and add the settings form to the
								// DOM. Bind the form's submit event
								// handler/callback.
								portlet.settings.element = $(
										portletSettingsHTML())
										.appendTo(
												$('.portlet-wrapper',
														portlet.element))
										.submit(portlet.saveSettings);
								// Bind the cancel button's event handler
								// too.
								portlet.settings.cancelButton = $(
										'.portlet-settings-cancel',
										portlet.settings.element).click(
										cancelEditSettings);
								// Build and add the inner form elements
								// from the HTML markup provided in the AJAX
								// data.
								portlet.settings.innerElement = $(
										'.portlet-settings-inner',
										portlet.settings.element).append(
										portlet.settings.markup);
								portlet.settings.ready = true;

								if (portlet.settings.displayed) {
									// If the user hasn't clicked away from
									// the settings pane, then display the
									// form.
									portlet.throbber.hide();
									portlet.showSettings();
								}

								getJavascript(portlet.settings.initScript);
							});
		}

		// Builds HTML for portlet settings forms.
		function portletSettingsHTML() {
			var html = '';
			html += '<form class="portlet-settings">';
			html += '  <div class="portlet-settings-inner"></div>';
			html += '  <div class="portlet-settings-buttons">';
			html += '    <input id="'
					+ portlet.id
					+ '-settings-save" class="portlet-settings-save" value="Save" type="submit" />';
			html += '    <input id="'
					+ portlet.id
					+ '-settings-cancel" class="portlet-settings-cancel" value="Cancel" type="submit" />';
			html += '  </div>';
			html += '</form>';
			return html;
		}

		// Initializes a generic portlet content throbber, for use by
		// settings form and external scripts.
		function initThrobber() {
			if (!portlet.throbber) {
				portlet.throbber = $(opts.throbberMarkup).appendTo(
						$('.portlet-wrapper', portlet.element));
			}
		}
		;

		// Event handler/callback for cancel button clicks.
		// @todo test this gets caught by all browsers when the cancel
		// button is 'clicked' via the keyboard.
		function cancelEditSettings() {
			portlet.toggleSettings();
			return false;
		}
		;

		// Helper function to execute external script on the server.
		// @todo It would be nice to provide some context to the script.
		// How?
		function getJavascript(url) {
			if (url) {
				$.getScript(url);
			}
		}
	};	
	

})(jQuery); // end of closure
